using System;
using System.Collections.Generic;
using NEnsure.Containers;

namespace NEnsure.Validators
{
	public class ManualValidator : IValidator
	{
		public event ValidationHandler<ValidationEventArgs> ValidationStart;

		public event ValidationHandler<EventArgs> ValidationEnd;

		public event ValidationHandler<PropertyRulesEventArgs> ValidatingProperty;

		public event ValidationHandler<PropertyRulesEventArgs> ValidatedProperty;

		private readonly IBrokenRuleBuilder _brokenRuleBuilder;

		private readonly List<IRuleContainer> _containers;

		private readonly BrokenRuleList _brokenRules;

		public IRuleContainer[] RuleContainers
		{
			get { return _containers.ToArray(); }
		}

		public BrokenRuleList BrokenRules
		{
			get { return _brokenRules; }
		}

		public ManualValidator()
			: this(null)
		{
			
		}

		public ManualValidator(IBrokenRuleBuilder brokenRuleBuilder)
		{
			_brokenRules = new BrokenRuleList();
			_containers = new List<IRuleContainer>();
			_brokenRuleBuilder = brokenRuleBuilder;
		}

		public IRuleContainer<T> Ensure<T>(string propertyName, T propertyValue)
		{
			if (string.IsNullOrEmpty(propertyName))
			{
				throw new ArgumentNullException("propertyName");
			}

			foreach(IRuleContainer container in _containers)
			{
				if (propertyName.Equals(container.PropertyName, StringComparison.OrdinalIgnoreCase))
				{
					IRuleContainer<T> actualContainer = container as IRuleContainer<T>;

					if (actualContainer == null)
					{
						throw new InvalidOperationException(
							string.Format(
								"Property {0} already exists for type {1}. " + 
									"Cannot add rules for same property with different type ({2}).", 
								propertyName, 
								container.GetType(), 
								typeof(T)));
					}

					if (!propertyValue.Equals(actualContainer.PropertyValue))
					{
						throw new ArgumentException(
							string.Format(
								"The same property {0} can't be added with two values (existing: '{1}', new: '{2}'.", 
								propertyName,
								actualContainer.PropertyValue, 
								propertyValue)
							, "propertyValue");
					}

					return actualContainer;
				}
			}

			IRuleContainer<T> newContainer = BuildContainer<T>(propertyName, propertyValue);

			_containers.Add(newContainer);

			return newContainer;
		}

		public IValidator Reset()
		{
			_containers.Clear();
			_brokenRules.Clear();

			return this;
		}

		public void EnsureAll()
		{
			if (shouldIContinue(fireValidationStart()))
			{
				foreach (IRuleContainer container in _containers)
				{
					if (!shouldIContinue(fireValidatingProperty(container)))
					{
						// TODO should we fire validatedproperty even if cancelled?
						continue;
					}

					if (!container.AreAllRulesSatisfied())
					{
						_brokenRules.AddRange(container.BrokenRules);
					}

					fireValidatedProperty(container);
				}	
			}

			fireValidationEnd();
		}

		private ValidationEventArgs fireValidationStart()
		{
			if (ValidationStart == null)
			{
				return null;
			}

			ValidationEventArgs e = new ValidationEventArgs(_containers.ToArray());

			ValidationStart(this, e);

			return e;
		}

		private PropertyRulesEventArgs fireValidatingProperty(IRuleContainer container)
		{
			if (ValidatingProperty == null)
			{
				return null; 
			}

			PropertyRulesEventArgs e = new PropertyRulesEventArgs(container.PropertyName, container.Rules);

			ValidatingProperty(this, e);

			return e;
		}

		private void fireValidationEnd()
		{
			if (ValidationEnd != null)
			{
				ValidationEnd(this, EventArgs.Empty);
			}
		}

		private void fireValidatedProperty(IRuleContainer container)
		{
			if (ValidatedProperty != null)
			{
				ValidatedProperty(this, new PropertyRulesEventArgs(container.PropertyName, container.Rules));
			}
		}

		private static bool shouldIContinue(ICancelable c)
		{
			if (c == null)
			{
				return true; 
			}

			return !c.Cancel;
		}

		private IRuleContainer<T> BuildContainer<T>(string propertyName, T propertyValue)
		{
			IRuleContainer<T> newContainer = _brokenRuleBuilder == null ? 
				new RuleContainer<T>(propertyName, propertyValue) : 
				new RuleContainer<T>(propertyName, propertyValue, _brokenRuleBuilder);

			return newContainer;
		}
	}
}
